package util.amazon;
import java.io.*;
import java.util.*;

import util.gen.*;
import org.jets3t.service.*;
import org.jets3t.service.impl.rest.httpclient.RestS3Service;
import org.jets3t.service.model.*;
import org.jets3t.service.multithread.*;
import org.jets3t.service.security.AWSCredentials;

/**Class to handle Amazon S3 file upload and download using the JetS3t API.
 * See http://jets3t.s3.amazonaws.com/index.html
 * @author Nix
 * */
public class S3Manager {

	//fields
	public String bucketName;
	public File credentialsFile;
	private String password;
	public AWSCredentials awsCredentials;
	public S3Service s3Service;
	public S3ServiceSimpleMulti simpleMulti;
	public S3Bucket bucket;
	public boolean connected;
	public Map threadedResults = null;


	//constructors
	/**Constructor, after instantiating check that boolean.connected is true;
	 * @param password - The password used to unlock the encrypted credentials file
	 * @param bucketName - Will attempt to create a bucket using this name if it doesn't exist, 
	 * remember they must be unique, a good naming convention is the accessKey+".bucketName".
	 * @param credentialsFile - encrypted accessKey and secretKey, see writeEncryptedCredentialsFile()
	 * */
	public S3Manager(String password, String bucketName, File credentialsFile) {
		//set fields
		this.password = password;
		this.bucketName = bucketName;
		this.credentialsFile = credentialsFile;
		connected = false;
		//load credentials
		if (loadCredentials() == false) return;
		//connect and test bucket
		if (connectAndTest() == false) return;
		connected = true;	
		//make synchronized hash map for holding threaded results
		threadedResults = Collections.synchronizedMap(new HashMap());
	}

	//methods
	/**Fetches the key names of the S3 objects. 
	 * Returns null if a problem is encountered.
	 * */
	public String[] fetchAllS3ObjectKeyNames(){
		try {
			S3Object[] obs = s3Service.listObjects(bucket);
			String[] keys = new String[obs.length];
			for (int i=0; i< obs.length; i++) keys[i] = obs[i].getKey();
			return keys;
		} catch (S3ServiceException e){
			System.err.println("Error: cannot fetch S3 object names.\n"+e);
			return null;
		}
	}
	
	/**Returns URLs to download particular objects from S3 that expire after a given amount of time.*/
	public String[] fetchTimedURLs(int minutes, String[] keys){
		//Determine what the time will be in x minutes.
        Calendar cal = Calendar.getInstance();
        cal.add(Calendar.MINUTE, minutes);
        Date expiryDate = cal.getTime();
        // Create signed HTTP GET URL valid for x minutes.
        String[] urls = new String[keys.length];
        try {
        	for (int i=0; i< keys.length; i++){
        		urls[i] = S3Service.createSignedGetUrl(bucket.getName(), keys[i], awsCredentials, expiryDate);
        	}
        } catch (S3ServiceException e){
			System.err.println("Error: cannot make signed URLs.\n"+e);
			return null;
        }
        return urls;
	}

	/**Upload files to S3. This will overwrite any file in S3 with the same name.*/
	public boolean upload (File[] files){
		try {
			// Create an array of data objects to upload.
			S3Object[] objects = new S3Object[files.length];
			for (int i=0; i< files.length; i++) {
				//does file exist?
				if (files[i].canRead() == false ){
					System.err.println("Error: cannot upload file to S3, it cannot be read or it doesn't exist. "+files[i]);
					return false;
				}
				objects[i] = new S3Object (bucket, files[i]);
			}
			// Upload multiple objects.
			simpleMulti.putObjects(bucket, objects);        

			return true;
		} catch (Exception e) {
			System.err.println("Error: problem with uploading files to S3.\n"+e);
			return false;
		}
	}

	/**Uploads files to S3 using a different thread.
	 * Check status by reading the Value in threadedResults Map 
	 * for the uniqueProcessName Key.
	 * If it says "Running" it's running, "Complete" it's complete, 
	 * anything else is an error.*/
	public boolean upload(String uniqueProcessName, File[] files){
		//check name doesn't exist in map
		if (threadedResults.containsKey(uniqueProcessName)) {
			System.err.println("Error: process S3 upload name is not unique, aborting. "+uniqueProcessName);
			return false;
		}

		//check files exist
		for (int i=0; i< files.length; i++){
			if (files[i] == null || files[i].canRead() == false){
				System.err.println("Error: one of your files for S3 upload is null or cannot be read, aborting.");
				return false;
			}
		}
		//add name
		threadedResults.put(uniqueProcessName, "Running");
		//make uploader
		new Upload(uniqueProcessName, files).start();
		return true;
	}

	/**For uploading files to S3 using threads.*/
	private class Upload extends Thread {
		File [] files;
		public Upload (String name, File[] files){
			this.setName(name);
			this.files = files;
		}
		public void run (){
			try {
				// Create an array of data objects to upload.
				S3Object[] objects = new S3Object[files.length];
				for (int i=0; i< files.length; i++) {
					objects[i] = new S3Object (bucket, files[i]);
				}
				// Create a simple multi-threading service
				S3ServiceSimpleMulti multi = new S3ServiceSimpleMulti(new RestS3Service(awsCredentials));
				// Upload multiple objects.
				multi.putObjects(bucket, objects);  
				threadedResults.put(getName(), "Complete");
			} catch (Exception e) {
				//add error to thread name
				String error = "Error: problem with uploading files to S3 using thread. "+ getName()+"\n"+e;
				System.err.println(error);
				threadedResults.put(getName(), error);
			}
		}
	}

	/**See threaded upload (String uniqueProcessName, File[] files).*/
	public boolean upload (String uniqueProcessName, File file){
		File[] f = new File[1];
		f[0] = file;
		return upload (uniqueProcessName, f);
	}

	/**See upload (File[] files).*/
	public boolean upload (File file){
		File[] f = new File[1];
		f[0] = file;
		return upload (f);
	}

	/**Downloads the keyed objects into the directory as files.
	 * Usefull for renaming the keyed objects when saving as files.
	 * @param dir - directory in which to save the files
	 * @param keys - String[] of S3 object key names
	 * @param fileNames - names to use in saving the files.*/
	public boolean download (File dir, String[] keys, String[] fileNames){
		//check dir exists
		if (dir == null || dir.canRead()== false){
			System.err.println("Error: cannot read or find your download directory, aborting. "+dir);
			return false;
		}
		// Create an array of data objects to download.
		DownloadPackage[] downloadPackages = new DownloadPackage[keys.length];
		for (int i=0; i< keys.length; i++) {
			downloadPackages[i] = new DownloadPackage(new S3Object (bucket, keys[i]), new File(dir, fileNames[i]));
		}
		// Download the objects.
		try {
			simpleMulti.downloadObjects(bucket, downloadPackages);
			return true;
		} catch (Exception e){
			System.err.println("Error: problem downloading files from S3.\n"+e);
			return false;
		}
	}
	
	/**Downloads files from S3 using a different thread.
	 * Check status by reading the Value in threadedResults Map 
	 * for the uniqueProcessName Key.
	 * If it says "Running" it's running, "Complete" it's complete, 
	 * anything else is an error.
	 * @param dir - directory to save files
	 * @param keys - object key names
	 * @param fileNames - what to rename each keyed object when saving as a file.*/
	public boolean download(String uniqueProcessName, File dir, String[] keys, String[] fileNames){
		//check name doesn't exist in map
		if (threadedResults.containsKey(uniqueProcessName)) {
			System.err.println("Error: process S3 download name is not unique, aborting. "+uniqueProcessName);
			return false;
		}
		//check dir exists
		if (dir == null || dir.canRead()== false){
			System.err.println("Error: cannot read or find your download directory, aborting. "+dir);
			return false;
		}
		//add name
		threadedResults.put(uniqueProcessName, "Running");
		//make uploader
		new Download(uniqueProcessName, dir, keys, fileNames).start();
		return true;
	}

	/**For downloading files from S3 using threads.*/
	private class Download extends Thread {
		File dir;
		String[] keys;
		String[] fileNames;

		public Download (String name, File dir, String[] keys, String[] fileNames){
			this.setName(name);
			this.dir = dir;
			this.keys = keys;
			this.fileNames = fileNames;
		}
		public void run (){
			try {
				// Create an array of data objects to download.
				DownloadPackage[] downloadPackages = new DownloadPackage[keys.length];
				for (int i=0; i< keys.length; i++) {
					downloadPackages[i] = new DownloadPackage(new S3Object (bucket, keys[i]), new File(dir, fileNames[i]));
				}
				// Download the objects.
				S3ServiceSimpleMulti multi = new S3ServiceSimpleMulti(new RestS3Service(awsCredentials));
				multi.downloadObjects(bucket, downloadPackages);
				threadedResults.put(getName(), "Complete");
			} catch (Exception e) {
				//add error to thread name
				String error = "Error: problem downloading files to S3 using thread. "+ getName()+"\n"+e;
				System.err.println(error);
				threadedResults.put(getName(), error);
			}
		}
	}

	/**Uses the file name as the S3 key to fetch and save the file.*/
	public boolean download (File file){
		File dir = file.getParentFile();
		String[] keys= {file.getName()};
		return download (dir, keys, keys);
	}

	/**Uses the file names as the S3 keys to fetch and save the files.*/
	public boolean download (File[] files){
		// Create an array of data objects to download.
		DownloadPackage[] downloadPackages = new DownloadPackage[files.length];
		for (int i=0; i< files.length; i++) {
			downloadPackages[i] = new DownloadPackage(new S3Object (bucket, files[i].getName()), files[i]);
		}
		// Download the objects.
		try {
			simpleMulti.downloadObjects(bucket, downloadPackages);
			return true;
		} catch (Exception e){
			System.err.println("Error: problem downloading files from S3.\n"+e);
			return false;
		}
	}

	/**Attempts to delete keyed objects from S3.*/
	public boolean delete (String[] keys){
		// Create an array of data objects to delete.
		S3Object[] objects = new S3Object[keys.length];
		for (int i=0; i< keys.length; i++) {
			objects[i] = new S3Object (bucket, keys[i]);
		}
		try {
			simpleMulti.deleteObjects(bucket, objects);
			return true;
		} catch (Exception e){
			System.err.println("Error: problem deleting files from S3.\n"+e);
			return false;
		}
	}

	/**Attempts to delete keyed object from S3.*/
	public boolean delete (String key){
		String[] f = new String[1];
		f[0] = key;
		return delete (f);
	}

	/**Write a password protected encripted credentials file.*/
	public static boolean writeEncryptedCredentialsFile (File credFile, String password, String accessKey, String secretKey){
		try {
			AWSCredentials awsCredentials = new AWSCredentials(accessKey, secretKey);
			awsCredentials.save(password, credFile);
			return true;
		} catch (Exception e){
			e.printStackTrace();
			return false;
		}
	}

	/**Attempts to create a AWSCredentials object from an encripted file.*/
	public boolean loadCredentials(){
		try {
			//does credentialsFile exist?
			if (credentialsFile.canRead() == false) {
				System.err.println("Error: cannot load credentials for S3, file cannot be read. Permissions? Does it exist? File -> "+credentialsFile);
				return false;
			}
			//attempt loading
			awsCredentials = AWSCredentials.load(password, credentialsFile);
			return true;
		} catch (S3ServiceException e){
			System.err.println("Error: cannot load S3 Service credentials from file, wrong password?\n "+e);
			return false;
		}
	}

	/**Instantiates connection objects and attempt to make a bucket if it doesn't exist to hold objects.*/
	public boolean connectAndTest(){
		try {
			//make object to interact with s3
			s3Service = new RestS3Service(awsCredentials);

			//get list of buckets, this call tests the connection
			S3Bucket[] myBuckets = s3Service.listAllBuckets();

			//make bucket?
			boolean found = false;
			for (int i=0; i< myBuckets.length; i++){
				if (myBuckets[i].getName().equals(bucketName)) {
					found = true;
					break;
				}
			}
			//will throw an error
			if (found == false) {
				System.out.println("Attempting to create an S3 bucket named "+bucketName);
				bucket = s3Service.createBucket(bucketName);
			}
			else bucket = new S3Bucket(bucketName);

			// Create a simple multi-threading service based on our existing S3Service
			simpleMulti = new S3ServiceSimpleMulti(s3Service);

			return true;
		} catch (S3ServiceException e){
			System.err.println("Error: problem with making a bucket or connecting to S3.\n"+e);
			return false;
		}
	}

	//main for testing
	public static void main (String[] args){
		//make manager
		S3Manager s3Man = new S3Manager("exGenPassWord", "0A4PT2HMEBJF9EDSBX82.ExGen.Raw", new File ("/Users/nix/Code/BioTools/Misc/awsCredentials.enc"));

		//check that it connected
		if (s3Man.isConnected()) System.out.println("Connected");
		else Misc.printExit("Not Connected");


		//upload a file
		long start = System.currentTimeMillis();
		File file = new File ("/Users/nix/Desktop/s_1_0001_prb.txt.zip");
		s3Man.upload(file);
		long stop = System.currentTimeMillis();
		System.out.println("\tTime: "+(stop-start));
		
		//get urls
		String[] urls = s3Man.fetchTimedURLs(5, new String[]{"s_1_0001_prb.txt.zip"});
		System.out.println(urls[0]);
		
		//delete
		System.out.println("Deleting");
		s3Man.delete(s3Man.fetchAllS3ObjectKeyNames());

		System.out.println("\nDone!");
	}


	public boolean isConnected() {
		return connected;
	}

	public AWSCredentials getAwsCredentials() {
		return awsCredentials;
	}

	public void setAwsCredentials(AWSCredentials awsCredentials) {
		this.awsCredentials = awsCredentials;
	}

	public S3Bucket getBucket() {
		return bucket;
	}

	public void setBucket(S3Bucket bucket) {
		this.bucket = bucket;
	}

	public String getBucketName() {
		return bucketName;
	}

	public void setBucketName(String bucketName) {
		this.bucketName = bucketName;
	}

	public File getCredentialsFile() {
		return credentialsFile;
	}

	public void setCredentialsFile(File credentialsFile) {
		this.credentialsFile = credentialsFile;
	}

	public S3Service getS3Service() {
		return s3Service;
	}

	public void setS3Service(S3Service service) {
		s3Service = service;
	}

	public S3ServiceSimpleMulti getSimpleMulti() {
		return simpleMulti;
	}

	public void setSimpleMulti(S3ServiceSimpleMulti simpleMulti) {
		this.simpleMulti = simpleMulti;
	}

	public void setConnected(boolean connected) {
		this.connected = connected;
	}

	public Map getThreadedResults() {
		return threadedResults;
	}

	public void setThreadedResults(Map threadedResults) {
		this.threadedResults = threadedResults;
	}


}
